Amazon Connect フローでの放棄呼と離脱箇所を、Step Functionsを利用しローコードでDynamoDBに保存してみた
はじめに
Amazon Connectのフローで離脱箇所や放棄呼をAmazon Kinesis Data Streams(以降、KDS)とStep Functionsを用いて取得し、Amazon DynamoDBに保存する方法をまとめました。
本記事の内容は以下のような用途に役立ちます。
- IVRでの途中離脱箇所を知りたい
- オペレーターにつながる前に切られる放棄呼の有無を知りたい
Connectは、各通話ごとに問い合わせレコード(Contact Trace Record, CTR)として通話記録を保存します。
Connectでは、KDSに問い合わせレコードを出力できます。通常は問い合わせレコードは、どのフローで切断されたか情報はありませんが、フロー内で工夫すると取得ができます。工夫内容は後述します。
以下の構成図に基づいて、処理の流れを説明します。
- Connectのフロー内で、図の青丸箇所のいずれかで切断したとします
- 切断直後、問い合わせレコードがKDSにストリーミングされます
- EventBridge PipesでソースをKDS、ターゲットをStep Functionsとし、Step Functionsで問い合わせレコードから切断情報などを抽出し、DynamoDBに保存します
以下のリソースを作成します。
- DynamoDB
- KDS
- Step Functions ステートマシン
- EventBridge Pipes
- Connectフロー
DynamoDB
以下の設定で作成します
- テーブル名:
call-records
- パーティションキー:
contact_id
今回の切断箇所以外にも他の情報をDynamoDBに保存することを想定し、パーティションキーはContact IDとします。
Step Functionsによって、DynamoDBには以下の属性を保存する想定です。
- コンタクトID
- 放棄呼の有無
- 発信日時
- フロー離脱箇所
- 発信元電話番号
KDS
KDSは、プロビジョンドモード、シャード1で作成します。
Step Functions ステートマシン
IAMロールでは以下のIAMポリシーを追加します
- AmazonDynamoDBFullAccess
EventBridge Pipes内でKDSから渡される内容は、以下のとおりです。
[ { "eventSource": "aws:kinesis", "eventVersion": "1.0", "eventID": "shardId-000000000000:xxxxxxx", "eventName": "aws:kinesis:record", "invokeIdentityArn": "arn:aws:iam::xxxxxxxxxxxx:role/service-role/Amazon_EventBridge_Pipe_cm-hirai-call-route_509278b3", "awsRegion": "ap-northeast-1", "eventSourceARN": "arn:aws:kinesis:ap-northeast-1:xxxxxxxxxxxx:stream/cm-hirai-call-route", "kinesisSchemaVersion": "1.0", "partitionKey": "f35752a8-261a-48bc-8d06-099b618e0e0c", "sequenceNumber": "49651139538302143095151058574031664653397744781153533954", "data": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "approximateArrivalTimestamp": 1714308481.275 } ]
上記のうち[0]['data']
は、base64でエンコードされているため、デコードすることで以下のような値が取得できます。
{ "AWSAccountId": "xxxxxxxxxxx", "AWSContactTraceRecordFormatVersion": "2017-03-10", "Agent": null, "AgentConnectionAttempts": 0, "AnsweringMachineDetectionStatus": null, "Attributes": { "call_completed": "false", "call_route": "start" }, "Campaign": { "CampaignId": null }, "Channel": "VOICE", "ConnectedToSystemTimestamp": "2024-04-17T23:47:52Z", "ContactDetails": {}, "ContactId": "9c905123-6240-4484-b9d1-4e3e40660d8e", "CustomerEndpoint": { "Address": "+81xxxxxxxxx", "Type": "TELEPHONE_NUMBER" }, "CustomerVoiceActivity": null, "DisconnectReason": "CUSTOMER_DISCONNECT", "DisconnectTimestamp": "2024-04-17T23:47:56Z", "InitialContactId": null, "InitiationMethod": "INBOUND", "InitiationTimestamp": "2024-04-17T23:47:52Z", "InstanceARN": "arn:aws:connect:ap-northeast-1:xxxxxxxxxxx:instance/3ff2093d-af96-43fd-b038-3c07cdd7609c", "LastUpdateTimestamp": "2024-04-17T23:49:03Z", "MediaStreams": [ { "Type": "AUDIO" } ], "NextContactId": null, "PreviousContactId": null, "Queue": null, "Recording": null, "Recordings": null, "References": [], "ScheduledTimestamp": null, "SegmentAttributes": { "connect:Subtype": { "ValueString": "connect:Telephony" } }, "SystemEndpoint": { "Address": "+81xxxxxxxxx", "Type": "TELEPHONE_NUMBER" }, "Tags": { "aws:connect:instanceId": "3ff2093d-af96-43fd-b038-3c07cdd7609c", "aws:connect:systemEndpoint": "+81xxxxxxxxx" }, "TaskTemplateInfo": null, "TransferCompletedTimestamp": null, "TransferredToEndpoint": null, "VoiceIdResult": null }
デコード後、Step Functionsによって、DynamoDBに以下を保存します。
- コンタクトID
- 放棄呼の有無
- 発信日時
- フロー離脱箇所
- 発信元電話番号
作成するステートマシンは以下の通りです。
[0]['data']
のデコードには、Step Functionsの組み込み関数を利用できます。
{ "decodedData.$": "States.Base64Decode($.[0].data)" }
デコード後、Step Functionsの組み込み関数を利用して、文字列からJSONに変換します。
{ "data.$": "States.StringToJson($.decodedData)" }
最後に、JSON形式に変換したデータから必要な情報をDynamoDBに保存します。
ステートマシンの全体のコードは、以下の通りです。
{ "Comment": "A description of my state machine", "StartAt": "Base64Decode", "States": { "Base64Decode": { "Type": "Pass", "Parameters": { "decodedData.$": "States.Base64Decode($.[0].data)" }, "Next": "ParseJSON" }, "ParseJSON": { "Type": "Pass", "Parameters": { "data.$": "States.StringToJson($.decodedData)" }, "Next": "DynamoDB UpdateItem" }, "DynamoDB UpdateItem": { "Type": "Task", "Resource": "arn:aws:states:::dynamodb:updateItem", "Parameters": { "TableName": "call-records-stepfunctions", "Key": { "contact_id": { "S.$": "$.data.ContactId" } }, "UpdateExpression": "SET phone_number = :phone , call_route = :route, start_time = :start, call_completed = :completed", "ExpressionAttributeValues": { ":phone": { "S.$": "$.data.CustomerEndpoint.Address" }, ":route": { "S.$": "$.data.Attributes.call_route" }, ":completed": { "S.$": "$.data.Attributes.call_completed" }, ":start": { "S.$": "$.data.InitiationTimestamp" } }, "ReturnValues": "UPDATED_NEW" }, "End": true } } }
DynamoDBに保存する属性は以下です
- コンタクトID:
contact_id
- 放棄呼の有無:
call_completed
- 発信日時:
start_time
- フロー離脱箇所:
call_route
- 発信元電話番号:
phone_number
EventBridge Pipes
Pipesを作成します。
ソースは、KDSを設定します。
ターゲットは、Step Functionsを設定します。
今回はターゲット入力トランスフォーマーを利用しませんでしたが、ターゲット先ではdata
のみを利用するため、トランスフォーマーを利用することも可能です。
Pipesを作成が完了です。
Amazon_EventBridge_Pipe_cm-hirai-call-route_509278b3
というIAMロール名が自動で作成されました。
以下のポリシーが追加されてます。
Connect フロー
本記事で利用するAmazon Connect のフローは以下の通りです。
フローは以下の流れです。
- 発信する
- アナウンスが流れるので、ユーザーがプッシュ式で「1」または「2」を入力します。
- 再度、アナウンスが流れるので、ユーザーがプッシュ式で「1」または「2」を入力します。
- 入力された内容がアナウンスされる
- 切断される
フロー (クリックすると展開します)
{ "Version": "2019-10-30", "StartAction": "83796d04-0550-4e7e-aa99-fce8b18c3f98", "Metadata": { "entryPointPosition": { "x": 241.6, "y": -47.2 }, "ActionMetadata": { "791eef75-931f-4116-8898-300cb47711db": { "position": { "x": 875.2, "y": 663.2 } }, "d6f52dbe-f77b-49ee-913e-11af21cf4f3b": { "position": { "x": 867.2, "y": 381.6 }, "dynamicParams": [] }, "0534b033-6e91-4b33-97f5-708c19b7aeec": { "position": { "x": 1324, "y": 138.4 } }, "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a": { "position": { "x": 2035.2, "y": 364 } }, "83796d04-0550-4e7e-aa99-fce8b18c3f98": { "position": { "x": 344, "y": -62.4 } }, "8ebcf8ff-9525-4b7e-b07e-b6699b340e2b": { "position": { "x": 859.2, "y": 200 }, "dynamicParams": [] }, "26cd035e-247c-4388-bff9-ba756df8235c": { "position": { "x": 1323.2, "y": 744.8 } }, "400c5522-9cb0-4054-93c1-1c813d715dee": { "position": { "x": 1084.8, "y": -184 }, "conditionMetadata": [ { "id": "f52c7bdd-2d72-468e-926f-e37f0fc37aab", "value": "1" }, { "id": "d6d3b356-0118-4876-97b7-4bd688c7f21b", "value": "2" } ] }, "a1972ceb-07b7-483e-b03f-2bd3dd6063fd": { "position": { "x": 1090.4, "y": 289.6 }, "conditionMetadata": [ { "id": "79e1cb23-1aa4-4841-8b71-b4723557d5f3", "value": "1" }, { "id": "d618d60a-c533-46dd-a605-c55459e451e1", "value": "2" } ] }, "0bb60df6-c203-413e-930c-0fe3eb2677ea": { "position": { "x": 349.6, "y": 110.4 }, "children": [ "c276029c-6058-4b01-91e5-159cf7c622f4" ], "overrideConsoleVoice": true, "fragments": { "SetContactData": "c276029c-6058-4b01-91e5-159cf7c622f4" }, "overrideLanguageAttribute": true }, "c276029c-6058-4b01-91e5-159cf7c622f4": { "position": { "x": 349.6, "y": 110.4 }, "dynamicParams": [] }, "1046bd11-9a7f-4746-a1eb-2ab571ceda91": { "position": { "x": 591.2, "y": 301.6 }, "conditionMetadata": [ { "id": "6b52ff60-18f7-4b45-81f3-02d69bb14d7f", "value": "1" }, { "id": "d3298e8f-e0bd-4aea-bad9-2ea2bc28c4cd", "value": "2" } ] }, "02b1970e-5964-40f3-8f02-a8a572528a8a": { "position": { "x": 349.6, "y": 289.6 }, "dynamicParams": [] }, "beb20743-c289-44c4-b15e-4296cd1524dc": { "position": { "x": 1323.2, "y": -48 }, "dynamicParams": [] }, "f8b767e9-6ce8-4d62-abe1-99ddb29a047b": { "position": { "x": 1823.2, "y": 292 } }, "2437cce9-a1c7-4b54-bd5e-48ee06780fcb": { "position": { "x": 1326.4, "y": -231.2 }, "dynamicParams": [] }, "9a65bea6-f010-4201-9b80-ba5fd949a730": { "position": { "x": 1322.4, "y": 343.2 }, "dynamicParams": [] }, "6ad4c36f-cde9-4895-9e2c-e3f0058300b0": { "position": { "x": 1324.8, "y": 553.6 }, "dynamicParams": [] }, "8cd3631c-4c50-486f-b1d0-0d19be958468": { "position": { "x": 1600.8, "y": 295.2 }, "dynamicParams": [] } }, "Annotations": [], "name": "cm-hirai-call-route", "description": "", "type": "contactFlow", "status": "published", "hash": {} }, "Actions": [ { "Parameters": { "Text": "有効な番号以外が入力されました。" }, "Identifier": "791eef75-931f-4116-8898-300cb47711db", "Type": "MessageParticipant", "Transitions": { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "Errors": [ { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "2" }, "TargetContact": "Current" }, "Identifier": "d6f52dbe-f77b-49ee-913e-11af21cf4f3b", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "Errors": [ { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Text": "有効な番号以外が入力されました。" }, "Identifier": "0534b033-6e91-4b33-97f5-708c19b7aeec", "Type": "MessageParticipant", "Transitions": { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "Errors": [ { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": {}, "Identifier": "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a", "Type": "DisconnectParticipant", "Transitions": {} }, { "Parameters": { "FlowLoggingBehavior": "Enabled" }, "Identifier": "83796d04-0550-4e7e-aa99-fce8b18c3f98", "Type": "UpdateFlowLoggingBehavior", "Transitions": { "NextAction": "0bb60df6-c203-413e-930c-0fe3eb2677ea" } }, { "Parameters": { "Attributes": { "call_route": "1" }, "TargetContact": "Current" }, "Identifier": "8ebcf8ff-9525-4b7e-b07e-b6699b340e2b", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "Errors": [ { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Text": "有効な番号以外が入力されました。" }, "Identifier": "26cd035e-247c-4388-bff9-ba756df8235c", "Type": "MessageParticipant", "Transitions": { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "Errors": [ { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "StoreInput": "False", "InputTimeLimitSeconds": "10", "Text": "1、もしくは、2、を押して下さい" }, "Identifier": "400c5522-9cb0-4054-93c1-1c813d715dee", "Type": "GetParticipantInput", "Transitions": { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "Conditions": [ { "NextAction": "2437cce9-a1c7-4b54-bd5e-48ee06780fcb", "Condition": { "Operator": "Equals", "Operands": [ "1" ] } }, { "NextAction": "beb20743-c289-44c4-b15e-4296cd1524dc", "Condition": { "Operator": "Equals", "Operands": [ "2" ] } } ], "Errors": [ { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "ErrorType": "InputTimeLimitExceeded" }, { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "ErrorType": "NoMatchingCondition" }, { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "StoreInput": "False", "InputTimeLimitSeconds": "10", "Text": "1、もしくは、2、を押して下さい" }, "Identifier": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "Type": "GetParticipantInput", "Transitions": { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "Conditions": [ { "NextAction": "9a65bea6-f010-4201-9b80-ba5fd949a730", "Condition": { "Operator": "Equals", "Operands": [ "1" ] } }, { "NextAction": "6ad4c36f-cde9-4895-9e2c-e3f0058300b0", "Condition": { "Operator": "Equals", "Operands": [ "2" ] } } ], "Errors": [ { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "ErrorType": "InputTimeLimitExceeded" }, { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "ErrorType": "NoMatchingCondition" }, { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "TextToSpeechEngine": "Neural", "TextToSpeechStyle": "None", "TextToSpeechVoice": "Kazuha" }, "Identifier": "0bb60df6-c203-413e-930c-0fe3eb2677ea", "Type": "UpdateContactTextToSpeechVoice", "Transitions": { "NextAction": "c276029c-6058-4b01-91e5-159cf7c622f4" } }, { "Parameters": { "LanguageCode": "ja-JP" }, "Identifier": "c276029c-6058-4b01-91e5-159cf7c622f4", "Type": "UpdateContactData", "Transitions": { "NextAction": "02b1970e-5964-40f3-8f02-a8a572528a8a", "Errors": [ { "NextAction": "02b1970e-5964-40f3-8f02-a8a572528a8a", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "StoreInput": "False", "InputTimeLimitSeconds": "10", "Text": "1、もしくは、2、を押して下さい" }, "Identifier": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "Type": "GetParticipantInput", "Transitions": { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "Conditions": [ { "NextAction": "8ebcf8ff-9525-4b7e-b07e-b6699b340e2b", "Condition": { "Operator": "Equals", "Operands": [ "1" ] } }, { "NextAction": "d6f52dbe-f77b-49ee-913e-11af21cf4f3b", "Condition": { "Operator": "Equals", "Operands": [ "2" ] } } ], "Errors": [ { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "ErrorType": "InputTimeLimitExceeded" }, { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "ErrorType": "NoMatchingCondition" }, { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "start", "call_completed": "false" }, "TargetContact": "Current" }, "Identifier": "02b1970e-5964-40f3-8f02-a8a572528a8a", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "Errors": [ { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "1-2" }, "TargetContact": "Current" }, "Identifier": "beb20743-c289-44c4-b15e-4296cd1524dc", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Text": "$.Attributes.call_route、が入力されました。終了します。" }, "Identifier": "f8b767e9-6ce8-4d62-abe1-99ddb29a047b", "Type": "MessageParticipant", "Transitions": { "NextAction": "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a", "Errors": [ { "NextAction": "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "1-1" }, "TargetContact": "Current" }, "Identifier": "2437cce9-a1c7-4b54-bd5e-48ee06780fcb", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "2-1" }, "TargetContact": "Current" }, "Identifier": "9a65bea6-f010-4201-9b80-ba5fd949a730", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "2-2" }, "TargetContact": "Current" }, "Identifier": "6ad4c36f-cde9-4895-9e2c-e3f0058300b0", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_completed": "true" }, "TargetContact": "Current" }, "Identifier": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "f8b767e9-6ce8-4d62-abe1-99ddb29a047b", "Errors": [ { "NextAction": "f8b767e9-6ce8-4d62-abe1-99ddb29a047b", "ErrorType": "NoMatchingError" } ] } } ] }
フローの離脱箇所
コンタクト属性は問い合わせレコードに保存されるため、分岐先情報をコンタクト属性として設定することで、フロー離脱箇所をDynamoDBに保存できます。
[コンタクト属性の設定]ブロックでフローの分岐情報として、キー:call_route
、値(分岐先)を付与します。
今回は以下のように、キー:call_route
の値は、数字にしていますが、問い合わせの種別(例えば、カードの紛失や口座開設希望など)を設定するとよいでしょう。
例えば、1
を入力後、切断すると、切断箇所は1
になります。1
を押して次のフローで2
を押して切断すると、1-2
が切断箇所になります。
放棄呼
放棄呼の有無は、発信直後とフローの最後に[コンタクト属性の設定]ブロックとして、キー:call_completed
、値(true or false)を付与することで確認できます。
オペレーターにつながるまでに電話を切られることを、放棄呼と指しますが、今回は最後まで進めたかどうかでtrue
or false
と判定します。フローの最後にオペレーターに繋がるようにしてもよいです。
例えば、発信し、最初の音声が流れた瞬間に切断すると、放棄呼がありとなりコンタクト属性はfalse
となります。最後まで進めると、放棄呼なしであり、コンタクト属性はtrue
になります。
試してみる
それでは実際に電話をかけて動作を確認してみましょう。
電話を終了してからDynamoDBに反映されるまでに、1分ほどかかります。
以下の結果となりました。
フローの離脱箇所はcall_route
で確認できます。例えば、call_route
が1
の場合は、最初の選択肢で1を選び、次の選択で切断されたということです。call_route
が2-1
の場合は、最初の選択で2を選び、次の選択で1を選んだ後に切断された場合です。
最後まで進めた場合(call_route
が1-2
や1-1
)、call_completed
がtrue
となり、放棄呼ではないということです。一方、発信直後に切断された場合は、call_completed
がfalse
で放棄呼があったことがわかります。
本記事の実装における改善点としては以下が挙げられます。
- 発信日時
start_time
は、UTCになっている。JSTに変換したい - 電話番号
phone_number
には、「+81」がついてる。「+81」を「0」に置き換えたい - 発信日時から切断までの通話時間も知りたい
上記は、Step Functionsのみでは実現できず、AWS Lambdaを利用する必要があります。
最後
本記事では、Amazon Connectのフローで離脱箇所や放棄呼をKDSとStep Functionsを用いて取得し、DynamoDBに保存する方法について解説しました。
Amazon Connect フローのコンタクト属性に分岐先情報を設定することで、離脱箇所や放棄呼の情報を比較的容易に取得できます。本記事が参考になれば幸いです。